/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.meta.service.impl.data.instance;

import java.util.ArrayList;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.AttributesPresentationBlockElement;
import org.unidata.mdm.core.type.model.BvtMapElement;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.model.GeneratingElement;
import org.unidata.mdm.core.type.model.GroupedElement;
import org.unidata.mdm.core.type.model.IndexedElement;
import org.unidata.mdm.core.type.model.LookupElement;
import org.unidata.mdm.core.type.model.ModelSearchEntry;
import org.unidata.mdm.core.type.model.NestedElement;
import org.unidata.mdm.core.type.model.PresentationElement;
import org.unidata.mdm.core.type.model.RegisterElement;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.core.type.model.RelationPresentationBlockElement;
import org.unidata.mdm.core.type.model.SearchableElement;
import org.unidata.mdm.core.type.model.ValidityPeriodElement;
import org.unidata.mdm.meta.service.LookupReferenceSupport;
import org.unidata.mdm.meta.type.instance.DataModelInstance;
import org.unidata.mdm.meta.type.model.entities.Entity;
import org.unidata.mdm.meta.type.model.entities.NestedEntity;
import org.unidata.mdm.search.type.IndexField;
import org.unidata.mdm.system.type.support.IdentityHashSet;

/**
 * @author Mikhail Mikhailov
 */
public class RegisterImpl extends AbstractBvtMapImpl<Entity>
    implements
        SearchableElement,
        IndexedElement,
        RegisterElement,
        PresentationElement,
        GroupedElement,
        LookupReferenceSupport {
    /**
     * Entry type 'REGISTER'.
     */
    private static final String ENTRY_TYPE_REGISTER = "REGISTER";
    /**
     * Incoming relations.
     */
    private final Map<RelationElement, EntityElement> incommingRelations = new IdentityHashMap<>();
    /**
     * Outgoing relations.
     */
    private final Map<RelationElement, EntityElement> outgoingRelations = new IdentityHashMap<>();
    /**
     * Lookups, refernced by this register.
     */
    private final Map<LookupElement, Set<AttributeElement>> referencedLookups = new IdentityHashMap<>();
    /**
     * Nested entities.
     */
    private final Set<NestedElement> referencedNesteds = new IdentityHashSet<>();
    /**
     * The index fields.
     */
    private final Map<String, IndexField> indexFields;
    /**
     * Autogenerated attributes.
     */
    private final List<AttributeElement> autogenerated;
    /**
     * Constructor.
     *
     * @param entity the entity
     * @param nested the raw nested elements
     * @param map global descending BVR map
     * @param dmi the instance
     */
    public RegisterImpl(final Entity entity, final List<NestedEntity> nested, Map<String, Integer> map, DataModelInstance dmi) {
        super(entity, nested, map);

        for (AttributeElement ae : getAttributes().values()) {

            if (ae.isLookupLink()) {
                LookupElement le = dmi.getLookup(ae.getLookupLink().getLookupLinkName());
                Objects.requireNonNull(le, "Lookup link attribute cannot reference a null object.");
                fixLookupLinkType(le, ae);
                referencedLookups.computeIfAbsent(le, k -> new IdentityHashSet<AttributeElement>()).add(ae);
                ((LookupImpl) le).putReferencingRegisters(this, referencedLookups.get(le));
            } else if (ae.isComplex()) {
                NestedElement ne = dmi.getNested(ae.getComplex().getNestedEntityName());
                Objects.requireNonNull(ne, "Complex attribute cannot reference a null object.");
                referencedNesteds.add(ne);
                ((NestedImpl) ne).addReferencingRegister(this);
            }
        }

        this.autogenerated = getAttributes().values().stream()
                .filter(AttributeElement::isGenerating)
                .collect(Collectors.toList());

        this.indexFields = getAttributes().entrySet().stream()
                .filter(e -> e.getValue().isIndexed())
                .map(e -> Pair.of(e.getKey(), e.getValue().getIndexed()))
                .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getId() {
        return getName();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isRegister() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public RegisterElement getRegister() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isValidityPeriod() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ValidityPeriodElement getValidityPeriod() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isBvtCapable() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public BvtMapElement getBvt() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isIndexed() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public IndexedElement getIndexed() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, IndexField> getIndexFields() {
        return indexFields;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isGenerating() {
        return Objects.nonNull(generator);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public GeneratingElement getGenerating() {
        return isGenerating() ? this : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasGeneratingAttributes() {
        return CollectionUtils.isNotEmpty(autogenerated);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<AttributeElement> getGeneratingAttributes() {
        return autogenerated;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isGrouped() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public GroupedElement getGrouped() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getGroupName() {
        return getSource().getGroupName();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasPresentation() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public PresentationElement getPresentation() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDashboardVisible() {
        return getSource().isDashboardVisible();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSearchable() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public SearchableElement getSearchable() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<ModelSearchEntry> getModelSearchEntries() {

        List<ModelSearchEntry> entries = new ArrayList<>();

        entries.add(new ModelSearchEntry(ENTRY_TYPE_REGISTER, getName(), ENTRY_TYPE_REGISTER, getName())
                .withEntryDisplayName(getDisplayName())
                .withEntryDescription(getDescription())
                .withDetail("group", getGroupName())
                .withDetail("dashboardVisible", String.valueOf(isDashboardVisible())));

        entries.addAll(getAttributeEntries(ENTRY_TYPE_REGISTER, getName()));

        return entries;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<RelationElement, EntityElement> getOutgoingRelations() {
        return outgoingRelations;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<RelationElement, EntityElement> getIncomingRelations() {
        return incommingRelations;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<LookupElement, Set<AttributeElement>> getReferencedLookups() {
        return referencedLookups;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<NestedElement> getReferencedNesteds() {
        return referencedNesteds;
    }
    /*
     * Package private post-processing.
     */
    void putIncommingRelation(RelationElement re, EntityElement target) {
        incommingRelations.put(re, target);
    }

    void putOutgoingRelation(RelationElement re, EntityElement target) {
        outgoingRelations.put(re, target);
    }

    @Override
    public Collection<RelationPresentationBlockElement> getRelationsPresentation() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Collection<AttributesPresentationBlockElement> getAttributesPresentation() {
        // TODO Auto-generated method stub
        return null;
    }
    // TODO Custom score for entities and lookups
    /*
     * private void calculateCustomScore(AbstractEntity entityDef) {
        Map<String, Float> scoreMapForUpdate = new HashMap<>();
        if (entityDef.getCustomProperties().stream()
                .anyMatch(customPropertyDef -> SCORE_ENABLE_PROPERTY.equals(customPropertyDef.getName())
                        && Boolean.parseBoolean(customPropertyDef.getValue()))) {

            for (AttributeModelElement attributeInfoHolder : getAttributesInfoMap(entityDef.getName()).values()) {
                ((AttributeInfoImpl) attributeInfoHolder).getAttribute().getCustomProperties().stream()
                        .filter(customPropertyDef -> SCORE_BOOST_FACTOR.equals(customPropertyDef.getName()))
                        .findFirst()
                        .ifPresent(customPropertyDef -> scoreMapForUpdate.put(attributeInfoHolder.getPath(),
                                Float.parseFloat(customPropertyDef.getValue())));
            }
            entityCustomScores.put(entityDef.getName(), scoreMapForUpdate);
        } else {
            entityCustomScores.remove(entityDef.getName());
        }
    }
     */
}
